// go-image-cli: 图片缩放 + 水印 + 并发处理 CLI 工具
//
// 本文件是一个单文件 CLI 工具示例，使用 github.com/disintegration/imaging
// 功能：
//  - 递归遍历输入目录或单个文件
//  - 并发缩放（Fit）图片到给定最大宽/高
//  - 在图片指定位置添加水印（自动缩放水印）
//  - 保存为 JPEG/PNG，可设置 JPEG quality
//  - 支持中断（Ctrl+C）和错误汇总
//
// 模块：
//   go mod init example.com/go-image-cli
//   go get github.com/disintegration/imaging

package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"image"
	"image/draw"
	"log"
	"math"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"syscall"

	"github.com/disintegration/imaging"
)

// Supported extensions (lowercase)
var exts = map[string]struct{}{
	".jpg":  {},
	".jpeg": {},
	".png":  {},
	".gif":  {},
	".webp": {},
}

func isImageFile(p string) bool {
	ext := strings.ToLower(filepath.Ext(p))
	_, ok := exts[ext]
	return ok
}

// watermarkPosition: one of "top-left","top-right","bottom-left","bottom-right","center"
func computeWatermarkPosition(base image.Image, wm image.Image, pos string, margin int) image.Point {
	bw := base.Bounds().Dx()
	h := base.Bounds().Dy()
	ww := wm.Bounds().Dx()
	wh := wm.Bounds().Dy()

	x := 0
	y := 0

	switch pos {
	case "top-left":
		x = margin
		y = margin
	case "top-right":
		x = bw - ww - margin
		y = margin
	case "bottom-left":
		x = margin
		y = h - wh - margin
	case "bottom-right":
		x = bw - ww - margin
		y = h - wh - margin
	case "center":
		x = (bw-ww)/2
		y = (h-wh)/2
	default:
		// fallback to bottom-right
		x = bw - ww - margin
		y = h - wh - margin
	}
	if x < 0 {
		x = 0
	}
	if y < 0 {
		y = 0
	}
	return image.Pt(x, y)
}

// scaleWatermark: 按照 base 图像尺寸按比例缩放水印
// scaleFactor 表示水印相对于 base 较短边的比例 (0.0-1.0)
func scaleWatermark(base image.Image, wm image.Image, scaleFactor float64) image.Image {
	if scaleFactor <= 0 || scaleFactor > 1 {
		return wm
	}
	bw := base.Bounds().Dx()
	h := base.Bounds().Dy()
	baseMin := math.Min(float64(bw), float64(h))

	target := int(math.Round(baseMin * scaleFactor))
	if target <= 0 {
		return wm
	}
	// preserve aspect ratio
	ww := wm.Bounds().Dx()
	wh := wm.Bounds().Dy()
	if ww == 0 || wh == 0 {
		return wm
	}
	ratio := float64(ww) / float64(wh)
	var newW, newH int
	if ratio >= 1 {
		newW = target
		newH = int(math.Round(float64(target) / ratio))
	} else {
		newH = target
		newW = int(math.Round(float64(target) * ratio))
	}
	if newW <= 0 || newH <= 0 {
		return wm
	}
	return imaging.Resize(wm, newW, newH, imaging.Lanczos)
}

// processImage: 读取 -> 缩放 -> 水印 -> 保存
func processImage(ctx context.Context, inPath, outPath string, maxW, maxH int, wm image.Image, wmScale float64, wmPos string, margin int, quality int, format string) error {
	// check ctx
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	img, err := imaging.Open(inPath)
	if err != nil {
		return fmt.Errorf("open %s: %w", inPath, err)
	}
	// resize preserving aspect ratio to fit within maxW x maxH
	if maxW > 0 || maxH > 0 {
		if maxW == 0 { // only maxH
			img = imaging.Resize(img, 0, maxH, imaging.Lanczos)
		} else if maxH == 0 { // only maxW
			img = imaging.Resize(img, maxW, 0, imaging.Lanczos)
		} else {
			img = imaging.Fit(img, maxW, maxH, imaging.Lanczos)
		}
	}

	// apply watermark if provided
	if wm != nil {
		// scale watermark relative to base image
		scaledWm := scaleWatermark(img, wm, wmScale)
		posPt := computeWatermarkPosition(img, scaledWm, wmPos, margin)

		// create a new NRGBA image to draw onto
		rgba := imaging.Clone(img)
		// imaging.Overlay does blending; but we'll use draw.Draw to ensure alpha preserved
		// convert scaledWm to NRGBA
		scaledNRGBA := imaging.Clone(scaledWm)
		// paste
		offset := image.Pt(posPt.X, posPt.Y)
		draw.Draw(rgba, image.Rectangle{Min: offset, Max: offset.Add(scaledNRGBA.Bounds().Size())}, scaledNRGBA, image.Point{0, 0}, draw.Over)
		img = rgba
	}

	// ensure out dir exists
	if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
		return err
	}

	// save
	switch strings.ToLower(format) {
	case "png":
		if err := imaging.Save(img, outPath); err != nil {
			return fmt.Errorf("save png %s: %w", outPath, err)
		}
	case "jpeg", "jpg":
		if err := imaging.Save(img, outPath, imaging.JPEGQuality(quality)); err != nil {
			return fmt.Errorf("save jpeg %s: %w", outPath, err)
		}
	default:
		// try to infer by extension
		if err := imaging.Save(img, outPath, imaging.JPEGQuality(quality)); err != nil {
			return fmt.Errorf("save %s: %w", outPath, err)
		}
	}
	return nil
}

// Walk input path to collect files
func collectFiles(input string) ([]string, error) {
	fi, err := os.Stat(input)
	if err != nil {
		return nil, err
	}
	var files []string
	if fi.IsDir() {
		err := filepath.Walk(input, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if info.IsDir() {
				return nil
			}
			if isImageFile(path) {
				files = append(files, path)
			}
			return nil
		})
		if err != nil {
			return nil, err
		}
	} else {
		if isImageFile(input) {
			files = append(files, input)
		} else {
			return nil, errors.New("input is not an image file")
		}
	}
	return files, nil
}

func main() {
	// flags
	var (
		input    = flag.String("input", "", "输入文件或目录 (required)")
		output   = flag.String("output", "output", "输出目录")
		maxW     = flag.Int("maxw", 1024, "最大宽度 (0 表示不限制)")
		maxH     = flag.Int("maxh", 0, "最大高度 (0 表示不限制)")
		workers  = flag.Int("workers", runtime.NumCPU(), "并发工人数")
		wmPath   = flag.String("watermark", "", "水印图片路径 (可选)")
		wmScale  = flag.Float64("wm-scale", 0.2, "水印相对较短边的比例 (0.0-1.0)")
		wmPos    = flag.String("wm-pos", "bottom-right", "水印位置: top-left/top-right/bottom-left/bottom-right/center")
		margin   = flag.Int("margin", 8, "水印与边缘的间距(px)")
		quality  = flag.Int("quality", 85, "输出 JPEG 质量 (1-100)")
		format   = flag.String("format", "jpeg", "输出格式: jpeg 或 png (默认 jpeg)")
		dryRun   = flag.Bool("dry", false, "仅打印将要执行的操作，不写文件")
	)
	flag.Parse()

	if *input == "" {
		flag.Usage()
		os.Exit(2)
	}

	files, err := collectFiles(*input)
	if err != nil {
		log.Fatalf("collect files: %v", err)
	}
	if len(files) == 0 {
		log.Fatalf("no image files found in %s", *input)
	}

	log.Printf("found %d files to process", len(files))

	// load watermark if any
	var wm image.Image
	if *wmPath != "" {
		w, err := imaging.Open(*wmPath)
		if err != nil {
			log.Fatalf("open watermark: %v", err)
		}
		wm = w
	}

	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer cancel()

	inCh := make(chan string)
	errCh := make(chan error, len(files))
	var wg sync.WaitGroup

	// workers
	for i := 0; i < *workers; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for path := range inCh {
				// build out path
				rel, _ := filepath.Rel(*input, path)
				if strings.HasPrefix(rel, "..") || *input == path {
					// when input is a file or outside, just use base name
					rel = filepath.Base(path)
				}
				outPath := filepath.Join(*output, rel)
				// ensure output extension matches format
				ext := strings.ToLower(filepath.Ext(outPath))
				if *format == "jpeg" || *format == "jpg" {
					outPath = strings.TrimSuffix(outPath, ext) + ".jpg"
				} else if *format == "png" {
					outPath = strings.TrimSuffix(outPath, ext) + ".png"
				}

				if *dryRun {
					log.Printf("[worker %d] dry: %s -> %s", id, path, outPath)
					continue
				}

				if err := processImage(ctx, path, outPath, *maxW, *maxH, wm, *wmScale, *wmPos, *margin, *quality, *format); err != nil {
					// if context canceled, propagate
					select {
					case <-ctx.Done():
						return
					default:
						errCh <- fmt.Errorf("%s -> %s: %w", path, outPath, err)
					}
				} else {
					log.Printf("[worker %d] done: %s -> %s", id, path, outPath)
				}
			}
		}(i)
	}

	// feeder
	go func() {
		defer close(inCh)
		for _, f := range files {
			select {
			case <-ctx.Done():
				return
			case inCh <- f:
			}
		}
	}()

	// wait
	wg.Wait()
	close(errCh)

	// collect errors
	var hadErr bool
	for e := range errCh {
		hadErr = true
		log.Printf("error: %v", e)
	}

	if ctx.Err() != nil {
		log.Printf("exited due to signal: %v", ctx.Err())
	}

	if hadErr {
		log.Printf("finished with errors")
		os.Exit(1)
	}
	log.Printf("all done")
}
